/*
 * NativeGenerator.java  0.4.0 / Nov 5, 2010
 *
 * Copyright (c) 2010 - 2011, cute4j.com and contributors.
 *
 * This software is licensed under the LGPL version 2.1 license.
 * A copy of this license is included in the file LICENSE-LGPL.txt.
 */
package com.cute4j.tools.ng;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;

import com.thoughtworks.qdox.JavaDocBuilder;
import com.thoughtworks.qdox.directorywalker.DirectoryScanner;
import com.thoughtworks.qdox.directorywalker.FileVisitor;
import com.thoughtworks.qdox.directorywalker.SuffixFilter;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaMethod;
import com.thoughtworks.qdox.model.JavaSource;

import com.cute4j.tools.ng.delegate.Delegate;
import com.cute4j.tools.ng.jni.JniCppFile;
import com.cute4j.tools.ng.jni.JniHFile;

/**
 * Native Code Generator.
 * <p>
 *  <tt>WARNING: Does not handle the unicode characters!</tt>
 * </p>
 *
 * @author Slobodan Vrkacevic
 * @version 0.4.0
 */
public final class NativeGenerator {

// Package-Private Static Fields. ----------------------------------------------

  static final NativeGenerator INSTANCE = new NativeGenerator();

// Private Fields. -------------------------------------------------------------

  private boolean verbose;
  private final JavaDocBuilder builder;
  private String newLine;
  private String copyrightNotice;
  private String classComment;
  private File jniFolder;
  private File delegateFolder;

// Private Constructor. --------------------------------------------------------

  private NativeGenerator() {
    builder = new JavaDocBuilder();
    newLine = "\r\n";
  }

// Public Methods. -------------------------------------------------------------

  /**
   * Returns verbose.
   *
   * @return the verbose
   */
  public boolean isVerbose() {
    return verbose;
  }

  /**
   * Sets verbose.
   *
   * @param verbose the verbose
   */
  public void setVerbose(boolean verbose) {
    this.verbose = verbose;
  }

  /**
   * Adds source file.
   *
   * @param file the source file
   *
   * @throws IOException if any I/O exception occurred
   */
  public void addSourceFile(String file) throws IOException {
    builder.addSource(new NativeFileReader(file), file);
  }

  /**
   * Adds source tree.
   *
   * @param dir the source tree directory
   */
  public void addSourceTree(String dir) {
    NativeFileCollector collector = new NativeFileCollector(builder);
    collector.collectFiles(dir);
  }

  /**
   * Adds source tree.
   *
   * @param dir the source tree directory
   */
  public void addSourceTree(File dir) {
    NativeFileCollector collector = new NativeFileCollector(builder);
    collector.collectFiles(dir);
  }

  /**
   * Returns copyright notice.
   *
   * @return copyright notice
   */
  public String getCopyrightNotice() {
    if (copyrightNotice == null) {
      String res = "resources/DefultCopyrightNotice.txt";
      InputStream is = NativeFileHeader.class.getResourceAsStream(res);
      BufferedReader br = new BufferedReader(new InputStreamReader(is));
      StringBuffer sb = new StringBuffer();
      try {
        for (int c = br.read(); c >= 0; c = br.read()) {
          sb.append((char)c);
        }
        copyrightNotice = sb.toString();
      } catch (IOException ioe) {
        copyrightNotice = " *";
      } finally {
        try {
          br.close();
        } catch (IOException ioe) {
          // empty catch block
        }
      }
    }
    return copyrightNotice;
  }

  /**
   * Returns copyright notice.
   *
   * @return copyright notice
   */
  public String getClassComment() {
    if (classComment == null) {
      String res = "resources/DefultClassComment.txt";
      InputStream is = NativeFileHeader.class.getResourceAsStream(res);
      BufferedReader br = new BufferedReader(new InputStreamReader(is));
      StringBuffer sb = new StringBuffer();
      try {
        for (int c = br.read(); c >= 0; c = br.read()) {
          sb.append((char)c);
        }
        classComment = sb.toString();
      } catch (IOException ioe) {
        classComment = " *";
      } finally {
        try {
          br.close();
        } catch (IOException ioe) {
          // empty catch block
        }
      }
    }
    return classComment;
  }

  /**
   * Sets copyright notice.
   *
   * @param copyrightNotice the copyright notice
   */
  public void setCopyrightNotice(String copyrightNotice) {
    this.copyrightNotice = copyrightNotice;
  }

  /**
   * Returns new line string.
   *
   * @return new line string
   */
  public String getNewLine() {
    return newLine;
  }

  /**
   * Sets new line.
   *
   * @param newLine the new line
   */
  public void setNewLine(String newLine) {
    this.newLine = newLine;
  }

  /**
   * Returns JNI folder.
   *
   * @return JNI folder
   */
  public File getJniFolder() {
    return jniFolder;
  }

  /**
   * Sets JNI folder.
   *
   * @param jniFolder the JNI folder
   */
  public void setJniFolder(File jniFolder) {
    this.jniFolder = jniFolder;
  }

  /**
   * Sets JNI folder.
   *
   * @param jniFolder the JNI folder
   */
  public void setJniFolder(String jniFolder) {
    this.jniFolder = new File(jniFolder);
  }

  /**
   * Returns delegate folder.
   *
   * @return delegate folder
   */
  public File getDelegateFolder() {
    return jniFolder;
  }

  /**
   * Sets delegate folder.
   *
   * @param delegateFolder the delegate folder
   */
  public void setDelegateFolder(File delegateFolder) {
    this.delegateFolder = delegateFolder;
  }

  /**
   * Sets delegate folder.
   *
   * @param delegateFolder the delegate folder
   */
  public void setDelegateFolder(String delegateFolder) {
    this.delegateFolder = new File(delegateFolder);
  }

  /**
   * Generates native files.
   *
   * @throws IOException if an I/O error occurred
   */
  public void generate() throws IOException  {
    if (jniFolder != null) {
      jniFolder.mkdirs();
    }
    if (delegateFolder != null) {
      delegateFolder.mkdirs();
    }
    JavaClass[] classes = builder.getClasses();
    for (int i = 0; i < classes.length; i++) {
      JavaClass javaClass = classes[i];
      if (!javaClass.isInner()) {
        if (jniFolder != null) {
          boolean generateJni = generateJni(javaClass);
          if (generateHeader(javaClass) || generateJni ||
              hasNativeMethods(javaClass)) {
            JniHFile jniHFile = new JniHFile(jniFolder, javaClass);
            jniHFile.store();
          }
          if (generateJni) {
            JniCppFile jniCppHFile = new JniCppFile(jniFolder, javaClass);
            jniCppHFile.store();
          }
        }
        if ((delegateFolder != null) && generateDelegate(javaClass)) {
          Delegate delegate = new Delegate(delegateFolder, javaClass);
          delegate.load();
          delegate.store();
        }
      }
    }
  }

// Public Static Methods. ------------------------------------------------------

  /**
   * Returns singleton instance of the native generator.
   *
   * @return singleton instance of the native generator
   */
  public static NativeGenerator getInstance() {
    return INSTANCE;
  }

  /**
   * Returns <tt>true</tt> if the class has native methods.
   *
   * @param javaClass the java class
   *
   * @return <tt>true</tt> if the class has native methods
   */
  public static boolean hasNativeMethods(JavaClass javaClass) {
    JavaMethod[] methods = javaClass.getMethods();
    for (int i = 0; i < methods.length; i++) {
      if (methods[i].isNative()) {
        return true;
      }
    }
    return false;
  }

  /**
   * Returns <tt>true</tt> if the class wants header file generation.
   *
   * @param javaClass the java class
   *
   * @return <tt>true</tt> if the class wants header file generation
   */
  public static boolean generateHeader(JavaClass javaClass) {
    String generate = Utils.getNativeTagParameter(javaClass, "generate");
    if ((generate != null) && generate.indexOf("header") >= 0) {
      return true;
    }
    return false;
  }

  /**
   * Returns <tt>true</tt> if the class wants jni files generation.
   *
   * @param javaClass the java class
   *
   * @return <tt>true</tt> if the class wants jni files generation
   */
  public static boolean generateJni(JavaClass javaClass) {
    //if (true) return true; // TODO: finish - remove
    String generate = Utils.getNativeTagParameter(javaClass, "generate");
    if ((generate != null) && generate.indexOf("jni") >= 0) {
      return true;
    }
    return false;
  }

  /**
   * Returns <tt>true</tt> if the class wants delegate files generation.
   *
   * @param javaClass the java class
   *
   * @return <tt>true</tt> if the class wants delegate files generation
   */
  public static boolean generateDelegate(JavaClass javaClass) {
    //if (true) return true;  // TODO: finish - remove
    String generate = Utils.getNativeTagParameter(javaClass, "generate");
    if ((generate != null) && generate.indexOf("delegate") >= 0) {
      return true;
    }
    return false;
  }

// Inner Classes. --------------------------------------------------------------

  /**
   * Native File Collector.
   */
  private static class NativeFileCollector {
    private final JavaDocBuilder builder;
    public NativeFileCollector(JavaDocBuilder builder) {
      this.builder = builder;
    }
    public void collectFiles(String dir) {
      DirectoryScanner scanner = new DirectoryScanner(new File(dir));
      scanner.addFilter(new SuffixFilter(".java"));
      scanner.scan(new NativeFileVisitor(builder));
    }
    public void collectFiles(File dir) {
      DirectoryScanner scanner = new DirectoryScanner(dir);
      scanner.addFilter(new SuffixFilter(".java"));
      scanner.scan(new NativeFileVisitor(builder));
    }
  }

  /**
   * Native File Collector.
   */
  private static class NativeFileVisitor implements FileVisitor {
    private final JavaDocBuilder builder;
    public NativeFileVisitor(JavaDocBuilder builder) {
      this.builder = builder;
    }
    public void visitFile(File currentFile) {
      NativeFileReader nativeFileReader = null;
      try {
        nativeFileReader = new NativeFileReader(currentFile);
        JavaSource javaSource = builder.addSource(nativeFileReader,
                                                  currentFile.toString());
        javaSource.setURL(currentFile.toURL());
      } catch (IOException e) {
        throw new RuntimeException("Could not read file: " +
                                   currentFile.getName());
      } finally {
        if (nativeFileReader != null) {
          try {
            nativeFileReader.close();
          } catch (IOException ioe) {
            ioe.printStackTrace();
          }
        }
      }
    }
  }

  /**
   * Native File Reader. Reads /*! comments.
   */
  static class NativeFileReader extends Reader {
    private static final int STATE_C0 = 0;
    private static final int STATE_C1 = 1;
    private static final int STATE_C2 = 2;
    private int state;
    private final FileReader fileReader;
    private NativeFileReader(FileReader fileReader) {
      state = STATE_C0;
      this.fileReader = fileReader;
    }
    public NativeFileReader(File file) throws FileNotFoundException {
      this(new FileReader(file));
    }
    public NativeFileReader(String file) throws FileNotFoundException {
      this(new FileReader(file));
    }
    public void close() throws IOException {
      fileReader.close();
    }
    public int read(char[] cbuf, int off, int len) throws IOException {
      int num = fileReader.read(cbuf, off, len);
      int offnum = off + num;
      for (int i = off; i < offnum; i++) {
        switch (state) {
          case STATE_C0:
            if (cbuf[i] == '/') {
              state = STATE_C1;
            }
            break;
          case STATE_C1:
            if (cbuf[i] == '*') {
              state = STATE_C2;
            } else {
              state = STATE_C0;
            }
            break;
          case STATE_C2:
            if (cbuf[i] == '!') {
              cbuf[i] = '*';
            }
            state = STATE_C0;
            break;
          default:
            throw new IllegalStateException("Invalid Reader State");
        }
      }
      return num;
    }
  }

}

